OpenAI Realtime 세션 시작 실패 원인 분석

마지막 업데이트 2026-07-29

OpenAI Realtime 세션 시작 실패 원인 분석: 증상: 수사 일지 ③ — 용의자 심문 1: instruction 문제 가능성, 원인: 수사 일지 ⑥ — 유력 용의자: 가장 가능성 높은 원인, 수정·검증: 최종 판단 흐름
동작 흐름 요약
  1. 증상: 수사 일지 ③ — 용의자 심문 1: instruction 문제 가능성
  2. 원인: 수사 일지 ⑥ — 유력 용의자: 가장 가능성 높은 원인
  3. 수정·검증: 최종 판단

분석 대상 로그: 6-019e3a4e-3cb5-7921-a41f-f00ff27f1335_0_467785f4-4286-4567-b0d3-93e687024a8c.json

주요 에러 시각: 2026-05-18T09:20:36.242Z

결론 — 유력 용의자부터 공개한다

The string did not match the expected pattern.는 OpenAI Realtime API가 직접 반환한 에러 메시지라기보다, 브라우저 WebRTC 엔진이 OpenAI Realtime 서버에서 받은 SDP answer 문자열을 RTCPeerConnection.setRemoteDescription()에서 파싱하다 실패한 런타임 에러일 가능성이 가장 높다.

현재 로그만으로는 instruction 문자열 문제라고 판단할 근거가 낮다. 로그에는 activity instruction 본문, OpenAI API 400/500 응답, Call creation failed, Invalid SDP response 같은 서버/API 실패 흔적이 없다.

수사 일지 ① — 사건 기록: 전체 로그 흐름

5/18 09:20 — 신고 접수

AI 세션이 시작되지 않았다는 신고. 현장에 남은 것은 [AI_SESSION] Failed to start session과 정체불명의 한 줄 — The string did not match the expected pattern. 누가 이 문장을 남겼는지부터가 수수께끼였다. 사건 전후의 로그를 시간순으로 복원했다.

시간 레벨 로그 해석
08:58:57 - 09:02:25 INFO Entry request failed, continuing in standalone mode
RoomNotFound
게스트 방 입장 요청이 실패했지만, 코드상 standalone mode로 계속 진행한다. 직접적인 AI 세션 실패 원인은 아니다.
09:08:04 - 09:19:42 WARN setSinkId failed: DOMException: A user gesture is required 사용자 제스처 없이 오디오 출력 장치를 지정하려다 브라우저가 거부했다. catch 후 경고만 남기므로 세션 시작을 중단시키지는 않는다.
09:20:36.242 ERROR [AI_SESSION] Failed to start session
The string did not match the expected pattern.
activityId: activity-1779091460856-4
AI 세션 시작 중 실제 실패가 발생했다. activityId는 실패한 activity 식별자이며, 그 자체가 instruction 문제를 의미하지 않는다.
09:20:36.243 ERROR [AI_SESSION] Failed to start session (handler) 내부 startSession 에러가 handler에서 재로깅된 것이다. 원인은 직전 에러와 동일하다.
초동 판단

앞선 두 이벤트(RoomNotFound, setSinkId)는 세션 시작을 중단시키지 않는 경로다. 사건은 09:20:36의 startSession 내부에서 일어났다. 범행 장소를 특정하려면 세션 시작 코드가 실제로 어떤 문자열을 다루는지 봐야 한다.

수사 일지 ② — 현장 검증: 관련 코드 경로

세션 시작 흐름은 다음과 같다.

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

const answer = await createSession(
  offer.sdp,
  activity,
  voice || avatar?.voice || DEFAULT_VOICE,
  user,
  avatar,
  subAvatars,
  realtimeModelVersion?.current,
  signal,
  turnDetectionRef.current.silence_duration_ms,
);

await pc.setRemoteDescription(answer);

answer.sdp는 우리 서버가 생성한 값이 아니라, 우리 서버가 OpenAI Realtime API https://api.openai.com/v1/realtime/calls에 offer SDP를 전달한 뒤 text로 받은 OpenAI 서버의 SDP answer다.

formData.set("sdp", sdp);
formData.set("session", JSON.stringify(sessionConfig));

const res = await fetch("https://api.openai.com/v1/realtime/calls", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
  },
  body: formData,
});

const answerSdp = await res.text();
용의자 소환

이 흐름에서 "패턴에 맞지 않는 문자열"을 만날 수 있는 지점은 셋 — instruction 문자열 처리, setSinkId 경고를 남긴 오디오 경로, 그리고 네트워크. 한 명씩 심문했다.

수사 일지 ③ — 용의자 심문 1: instruction 문제 가능성

낮음.

instruction 문제가 직접 원인이라면 보통 다음과 같은 흔적이 있어야 한다.

제공된 로그에는 위 흔적이 없다. 또한 instruction 치환 코드는 정규식 기반 문자열 처리이며, 해당 메시지를 직접 생성하는 경로가 확인되지 않았다.

수사 일지 ④ — 용의자 심문 2: setSinkId 로그와의 관계

직접 관련은 낮음.

DOMException: A user gesture is requiredaudioEl.setSinkId(audioOutputDeviceId)에서 발생한다. 이 호출은 try/catch로 감싸져 있고 실패해도 경고만 남긴 뒤 세션 시작 흐름은 계속 진행된다.

try {
  await audioEl.setSinkId(audioOutputDeviceId);
} catch (e) {
  console.warn("setSinkId failed:", e);
}

따라서 이 경고는 원하는 출력 장치로 소리가 나지 않을 수 있다는 신호이지, The string did not match the expected pattern. 세션 실패의 직접 원인으로 보기는 어렵다.

수사 일지 ⑤ — 용의자 심문 3: 네트워크 문제 가능성

직접 가능성은 낮고, 간접 가능성은 제한적으로 있음.

일반적인 네트워크 문제라면 보통 다음 에러가 나온다.

현재 에러는 이미 받은 문자열을 브라우저가 특정 패턴으로 파싱하다 실패하는 형태다. 다만 프록시/서버 문제로 SDP 응답이 변형되거나 잘못 전달된다면 간접적으로 비슷한 현상이 날 수 있다.

수사망 좁히기

세 용의자 모두 알리바이가 성립했다. 남는 것은 코드 경로의 마지막 줄 — 외부(OpenAI)에서 받은 SDP answer 문자열을 브라우저 WebRTC 파서에 넘기는 setRemoteDescription(). "패턴에 맞지 않는 문자열"이라는 문장과 정확히 맞아떨어지는 유일한 지점이다.

수사 일지 ⑥ — 유력 용의자: 가장 가능성 높은 원인

  1. OpenAI Realtime SDP answer와 현재 브라우저 WebRTC 파서의 호환성 문제
    특히 Safari/WebKit 계열이면 SDP 파싱이 더 엄격할 수 있다.
  2. SDP 문자열 형식 문제
    줄바꿈, 지원하지 않는 SDP attribute, codec/header extension 등으로 setRemoteDescription()에서 실패했을 수 있다.
  3. 응답 객체 검증 부족
    answer.type, answer.sdp, SDP 앞부분/길이를 실패 직전에 기록하지 않아 현재 로그만으로 확정이 어렵다.
검거 준비

유력 용의자는 특정됐지만 결정적 물증(실패 순간의 SDP 내용)이 없어 검거는 미완이다. 다음 범행 때 현장을 그대로 붙잡을 수 있도록 함정(로그 보강)과 방어선(검증·정규화·재시도)을 깐다.

검거 준비 — 권장 대처

1. 실패 지점 로그 보강

try {
  await pc.setRemoteDescription(answer);
} catch (error) {
  logger.error("Failed to apply realtime answer", {
    errorName: error instanceof Error ? error.name : undefined,
    errorMessage: error instanceof Error ? error.message : String(error),
    errorStack: error instanceof Error ? error.stack : undefined,
    answerType: answer?.type,
    sdpStartsWithV: answer?.sdp?.startsWith("v="),
    sdpLength: answer?.sdp?.length,
    sdpHead: answer?.sdp?.slice(0, 300),
    activityId: activity.id,
  });
  throw error;
}

2. setRemoteDescription 전 answer 검증

if (
  answer?.type !== "answer" ||
  typeof answer.sdp !== "string" ||
  !answer.sdp.startsWith("v=")
) {
  throw new Error("Invalid realtime answer SDP");
}

3. SDP 줄바꿈 정규화

const normalizedAnswer = {
  type: "answer" as const,
  sdp: answer.sdp.replace(/\r?\n/g, "\r\n"),
};

await pc.setRemoteDescription(normalizedAnswer);

4. 1회 재시도

setRemoteDescription 실패 시 기존 RTCPeerConnection을 닫고, offer 생성부터 OpenAI Realtime call 생성까지 한 번만 재시도한다. 같은 SDP answer를 재사용하지 않는 것이 중요하다.

5. Safari/WebKit 대응

추가 확인에 필요한 로그

최종 판단

현재 로그 전체 흐름에서는 AI 세션 시작 중 WebRTC remote SDP 적용 실패가 가장 타당한 원인이다. instruction 문자열 문제, 일반 네트워크 문제, setSinkId 사용자 제스처 문제는 직접 원인으로 보기 어렵다.

다음 재현 시에는 SDP answer와 error metadata를 남기도록 로그를 보강해야 원인을 확정할 수 있다.